using System.Diagnostics;
using System.Net;
using HelpDeskTool.Entities;
using HelpDeskTool.Extensions;
using HelpDeskTool.Utils;
using Microsoft.Extensions.Logging;

namespace HelpDeskTool.Services;

public class Updater : IUpdater
{
	public event EventHandler? UpdateEnded;
	public event EventHandler? InformationChanged;

	private readonly ILdap ldap;
	private readonly IWmi wmi;
	private readonly IDns dns;
	private readonly ILogger logger;

	private static bool isRun;
	private static readonly TimeSpan timeoutOnError = TimeSpan.FromSeconds(5);

	public Updater(ILdap ldap, IWmi wmi, IDns dns, ILogger<Updater> logger)
	{
		this.ldap = ldap;
		this.wmi = wmi;
		this.dns = dns;
		this.logger = logger;
	}

	public async Task StartAsync(ICollection<IComputer> computers, TimeSpan period, CancellationToken cancellationToken = default)
	{
		if (isRun)
			throw new Exception("Already started instance!");

		isRun = true;

		var parallelOptions = new ParallelOptions
		{
			MaxDegreeOfParallelism = Environment.ProcessorCount,
			CancellationToken = cancellationToken
		};

		var dog = new Stopwatch();

		while (!cancellationToken.IsCancellationRequested)
		{
			try
			{
				logger.LogInformation("Updating task start");

				dog.Restart();
				await Do(computers, parallelOptions);
				dog.Stop();

				logger.LogInformation($"Time spent updating task: {dog.Elapsed.TotalSeconds}");
				logger.LogInformation($"Updating task will sleep for {period.TotalSeconds} sec.");

				UpdateEnded?.Invoke(this, EventArgs.Empty);

				await Task.Delay(period, cancellationToken);
			}
			catch (OperationCanceledException)
			{
				isRun = false;

				throw;
			}
			catch (Exception e)
			{
				logger.LogException(e);
				logger.LogInformation($"Updating task will restart in {timeoutOnError.TotalSeconds} sec.");

				await Task.Delay(timeoutOnError, cancellationToken);
			}
		}
	}

	public async Task<DetailInfo> GetInformationAsync(string hostname, CancellationToken cancellationToken = default)
	{
		WmiInfo? wmiInfo = default;
		IPAddress[]? ipAddresses = default;

		var parallelOptions = new ParallelOptions
		{
			MaxDegreeOfParallelism = Environment.ProcessorCount,
			CancellationToken = cancellationToken
		};

		await Tasks.TryParallelRunAsync(logger.LogException, parallelOptions,
			async () => ipAddresses = await dns.GetIpAddressAsync(hostname, cancellationToken),
			async () => wmiInfo = await wmi.GetInfoAsync(hostname, cancellationToken));

		return new DetailInfo
		{
			Hardware = wmiInfo?.Hardware,
			OperatingSystem = wmiInfo?.OperatingSystem,
			LastBootUp = wmiInfo?.LastBootUp,
			Updated = wmiInfo?.Updated,
			IpAddresses = ipAddresses,
		};
	}

	private async Task Do(ICollection<IComputer> computers, ParallelOptions parallelOptions)
	{
		var updates = await ldap.GetComputersAsync(parallelOptions.CancellationToken);

		UpdateCollection(computers, updates);

		await Parallel.ForEachAsync(computers, parallelOptions, async (computer, token) =>
		{
			try
			{
				var info = await GetInformationAsync(computer.Name, token);
				computer.Information = computer.Information.Merge(info);

				InformationChanged?.Invoke(this, EventArgs.Empty);
			}
			catch (OperationCanceledException)
			{
				throw;
			}
			catch (Exception e)
			{
				logger.LogException(e);
			}
		});
	}

	private static void UpdateCollection(ICollection<IComputer> destination, IEnumerable<IComputer> source)
	{
		App.Current.Dispatcher.Invoke(() =>
		{
			var added = source.Except(destination);
			var deleted = destination.Except(source);

			destination
				.RemoveRange(deleted)
				.AddRange(added);
		});
	}
}
